package at.ac.tuwien.dse.health.util;

import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet;
import com.google.common.reflect.ClassPath;
import com.google.common.testing.ArbitraryInstances;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.BeanUtils;
import org.springframework.util.ReflectionUtils;

import java.beans.PropertyDescriptor;
import java.io.IOException;

import static com.google.common.collect.Iterables.transform;

/**
 * Runs getter and setter checks against top level classes in the same package and every subpackage of a certain class.
 *
 * @author Bernhard Nickel
 * @author Gregor Schauer
 * @author Dominik Strasser
 */
public class BeanPropertyTester {
	private static final Logger LOGGER = LogManager.getFormatterLogger(BeanPropertyTester.class);
	protected Class<?> clazz;

	public BeanPropertyTester(Class<?> clazz) {
		this.clazz = clazz;
	}

	/**
	 * Attempts invoke getter and setter methods on all object instances created from the classes found.
	 */
	public void testBeanProperties() {
		for (Class clazz : getClasses(this.clazz)) {
			try {
				Object obj = BeanUtils.instantiateClass(clazz);
				LOGGER.debug("Testing instance of type %s %s", clazz, obj.toString());
				PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(obj.getClass());
				for (PropertyDescriptor descriptor : descriptors) {
					try {
						Object val = descriptor.getReadMethod() != null
								? ReflectionUtils.invokeMethod(descriptor.getReadMethod(), obj)
								: ArbitraryInstances.get(descriptor.getWriteMethod().getReturnType());
						if (descriptor.getWriteMethod() != null) {
							ReflectionUtils.invokeMethod(descriptor.getWriteMethod(), obj, val);
						}
					} catch (Exception e) {
						LOGGER.debug("Cannot test bean property %s.%s: %s", clazz.getName(), descriptor.getName(), ExceptionUtils.getMessage(e));
					}
				}
			} catch (Exception e) {
				/**
				 * Happens if class is not instantiable i.e., it is an interface, an annotation, an abstract class,
				 * an enum, a non-static inner class or has no accessible default constructor.
				 */
			}
		}
	}

	/**
	 * Loads all top level classes located in the same or any sub package of the given class lazily.
	 *
	 * @param clazz the class
	 * @return the classes found
	 */
	private static Iterable<Class<?>> getClasses(Class<?> clazz) {
		ImmutableSet<ClassPath.ClassInfo> classInfo = getClassPath(clazz).getTopLevelClassesRecursive(clazz.getPackage().getName());
		return transform(classInfo, new Function<ClassPath.ClassInfo, Class<?>>() {
			@Override
			public Class<?> apply(ClassPath.ClassInfo input) {
				return input.load();
			}
		});
	}

	/**
	 * Returns a {@code ClassPath} representing all classes and resources loadable from the given classes
	 * {@link ClassLoader} and its parent {@link ClassLoader ClassLoaders}.
	 *
	 * @param clazz the class
	 * @return the classpath
	 * @throws RuntimeException if the attempt to read class path resources (jar files or directories) failed
	 */
	private static ClassPath getClassPath(Class<?> clazz) {
		try {
			return ClassPath.from(clazz.getClassLoader());
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}
}
